<?php
namespace phiction;
use \phiction\exceptions\unimplemented;
use \phiction\string_utils as str;

class migrate
{
    private $plan;

    function __construct(schema_loader $schema_ld)
    {
        $this->plan = self::build_plan($schema_ld);
    }

    function plan(): string
    {
        return $this->plan;
    }

    function run(\PDO $pdo)
    {
        $pdo->exec($this->plan());
    }

    private static function build_plan(schema_loader $schema_ld): string
    {
        $plan = [];
        $schemas = $schema_ld->schemas;

        # drop all tables
        $plan[] = "";
        $plan[] = "-------- drop tables --------";
        $plan[] = "";
        foreach ($schemas as $name => $schema) {
            $table = self::sql_name($name);
            $plan[] = "DROP TABLE IF EXISTS $table CASCADE;";
        }
        $plan[] = "";

        # create all tables, without foreign keys
        $plan[] = "";
        $plan[] = "-------- create tables without foreign keys --------";
        $plan[] = "";
        foreach ($schemas as $name => $schema) {
            $fields = [];
            foreach ($schema as $field => $type)
                $fields[] = self::field_to_sql($field, $type);
            $fields = str::indent(implode(",\n", $fields));

            $table = self::sql_name($name);
            $plan[] = "CREATE TABLE $table(";
            $plan[] = $fields;
            $plan[] = ");";
            $plan[] = "";
        }

        # add foreign keys
        $plan[] = "";
        $plan[] = "-------- add foreign keys --------";
        $plan[] = "";
        foreach ($schemas as $name => $schema) {
            $actions = [];
            foreach ($schema as $field => $type) {
                if ($type['type'] === 'table') {
                    $constraint = self::sql_name($name . "_" . $field . "_fkey");
                    $field_name = self::sql_name($field);
                    $ref_table = self::sql_name($type['name']);
                    $ref_field = self::sql_name($schema_ld->identity_field($type['name']));
                    $actions[] = "ADD CONSTRAINT $constraint FOREIGN KEY ($field_name) REFERENCES $ref_table($ref_field) ON DELETE CASCADE";
                }
            }
            if (count($actions) !== 0) {
                $table = self::sql_name($name);
                $plan[] = "ALTER TABLE $table";
                $plan[] = str::indent(implode(",\n", $actions) . ";");
                $plan[] = "";
            }
        }

        $plan = implode("\n", $plan) . "\n";
        return $plan;
    }

    private static function field_to_sql(string $field, array $type): string
    {
        $nullable = $type['nullable'] ? "NULL" : "NOT NULL";
        $field = self::sql_name($field);
        $type_name = self::sql_type($type['type']);
        if (array_key_exists('max-length', $type)) {
            $max_len = $type['max-length'];
            if (!is_null($max_len))
                $type_name .= " CHECK(length($field) <= $max_len)";
        }
        return "$field $type_name $nullable";
    }

    private static function sql_name(string $name): string
    {
        return '"' . $name . '"';
    }

    private static function sql_type(string $type): string
    {
        switch ($type) {
            case 'identity': return 'serial PRIMARY KEY';
            case 'int': return 'integer';
            case 'string': return 'text';
            case 'table': return 'integer';
            case 'sequence': return self::sql_name($type['name']);
            case 'enum': return self::sql_name($type['name']);
            default: throw new \InvalidArgumentException("unknown type: $type");
        }
    }
}

